Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PROD-30967: Hux implementation #4118

Merged
merged 6 commits into from
Dec 3, 2024
Merged

PROD-30967: Hux implementation #4118

merged 6 commits into from
Dec 3, 2024

Conversation

denis-getopensocial
Copy link
Contributor

@denis-getopensocial denis-getopensocial commented Oct 16, 2024

Problem

As outlined in our decision record, we have identified the need to integrate Hux instead of relying on traditional hooks for managing interactions and events within our software. The use of hooks, while functional, has led to limitations in scalability, maintainability, and flexibility.

Solution

We will migrate from the current hook-based implementation to Hux. The Hux library offers a more modern and modular approach for handling events, making the system easier to maintain and extend in the long term.
Blog post on how to: https://www.previousnext.com.au/blog/hux-alternative-to-hooks
Drupal seems to go the same way: https://www.drupal.org/node/3442349

Issue tracker

https://getopensocial.atlassian.net/browse/PROD-30967

Theme issue tracker

How to test

Screenshots

Release notes

Change Record

Creation of a current group service to fetch the current group service from the context.

Translations

@denis-getopensocial denis-getopensocial added type: feature Adds a new feature to Open Social status: needs review This pull request is waiting for a requested review needs work: tests This pull request is missing test coverage prio: medium team: guardians labels Oct 16, 2024
@denis-getopensocial denis-getopensocial self-assigned this Oct 16, 2024
@denis-getopensocial denis-getopensocial changed the base branch from main to issue/PROD-30376/upgrade-drupal-to-10.3 October 16, 2024 08:20
@denis-getopensocial denis-getopensocial changed the title Feature/hux d10.3 Prod-30967: Hux implementation Oct 16, 2024
use Drupal\social_post\Service\SocialPostHelperInterface;


final class SocialPostFormHooks {
Copy link
Contributor Author

@denis-getopensocial denis-getopensocial Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wanted to have 1 class per hook replacement for better readability and maintenance but it will break the OS rule about not extra naming what we should follow. If I have 1 class per hook replacement the className would have the same name as the method (kind of).
IE: replacement of social_post_form_post_form_alter
ClassName SocialPostFormPostFormAlterHook, method called SocialPostFormPostFormAlterHook.
I'm really up for suggestions here if we group hooks together or not in a class? (my preference is not) And then how do we name the class and the method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first instinct would be to split it in different classes. There will likely be cases where we it makes more sense to group them, e.g. when multiple hooks execute the same underlying code.

And I would ignore the rule about extra naming. Maybe it looks a bit weird, but while reading it you know what it does 🤷

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I look at what the hook does in this case then it's preparing the post form to be personalised to the user and to the context?

What about Drupal\social_post\Hooks\PostFormPersonalisation as fully qualified name? It indicates it's doing something to the Post form and it's personalising. "Social" is omitted because that's included in the namespace already which IDEs also include in their class search.

@denis-getopensocial denis-getopensocial force-pushed the feature/hux-d10.3 branch 7 times, most recently from e3f05d8 to 8e34027 Compare October 16, 2024 13:06
@denis-getopensocial denis-getopensocial changed the base branch from main to feautre/module-handler-update October 21, 2024 08:20
Base automatically changed from feautre/module-handler-update to main October 22, 2024 12:45
@open-social-tugboat
Copy link

Tugboat has finished building the preview for this pull request!

Link:

Dashboard:

@denis-getopensocial denis-getopensocial changed the title Prod-30967: Hux implementation PROD-30967: Hux implementation Oct 23, 2024
Copy link
Member

@bramtenhove bramtenhove left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@denis-getopensocial thanks a lot for the hard work!

I looked through all the code and I think this is definitely much better than the form_alters we've applied before. It's great to see additional unit testing is made possible and easier this way!

I left a few comments on files and your own remarks.

use Drupal\social_post\Service\SocialPostHelperInterface;


final class SocialPostFormHooks {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first instinct would be to split it in different classes. There will likely be cases where we it makes more sense to group them, e.g. when multiple hooks execute the same underlying code.

And I would ignore the rule about extra naming. Maybe it looks a bit weird, but while reading it you know what it does 🤷

Copy link
Member

@Kingdutch Kingdutch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this review took a while :D While I was writing this review, Bram also commented on some things so there may be some overlap :)

I realise some of the things I'm reviewing might be debatable here, which is also why I think this PR might even be split into 3 PRs:

  1. Add the hux dependency (that's already approved and non-controversial)
  2. Rewrite the way to get the current group (we have a few smaller points there) which might even be used by the current hook implementation. It comes to light because we're using Hux but it's an improvement we may still want to make even if we wouldn't use Hux, so it's really an independent change
  3. Rewrite the hook to Hux which is where the larger discussion is currently ongoing

I'm trying to get other developers to also think more about "What are the smallest independent changes that we can make".

Comment on lines 53 to 57
social_group.current_group:
class: Drupal\social_group\CurrentGroupService
arguments:
- '@entity_type.manager'
- '@group.group_route_context'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coding guideline 3.6 Use service autowiring and alias interfaces to concrete ID'd service definitions suggests some changes here.

Preferably the service itself would be an interface that would be used for type-hinting, but I don't think that makes sense here.

Suggested change
social_group.current_group:
class: Drupal\social_group\CurrentGroupService
arguments:
- '@entity_type.manager'
- '@group.group_route_context'
Drupal\social_group\CurrentGroupService:
class: Drupal\social_group\CurrentGroupService
autowire: true
social_group.current_group: '@Drupal\social_group\CurrentGroupService'

(Drupal Core sets the alias the other way around but I don't really understand why, since Symfony's preferrable way is to use interfaces rather than distinct names).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can create an Interface for every things we create but in that case I dont see the point. If i s required I'm happy to comply otherwise i think it s unnecessary artefact.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agreed in my message that an interface may not be necessary here. We should still make sure that we use autowiring and define our services in a way so that this class can be used by another service that uses autowiring. That's what my suggested code snippet achieves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

ContextProviderInterface $group_route_context,
) {
$this->entityTypeManager = $entity_type_manager;
$this->groupRouteContext = $group_route_context;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing the context system in this way was new to me and I was wondering whether we should be calling into the group service directly like this.

While figuring out how the system works I came about the context.repository system which we can also use by providing the service name and context (@group.group_route_context:group) to its getRuntimeContexts methods. The benefit of using that system is that it will also statically cache the context value so it's not recomputed in case the method is called multiple times.

What do you think of moving this over to Drupal's context repository rather than calling a system directly? It also means that if we want to conditionally pull from multiple contexts in the future we don't need another service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh i was looking at how to, and the only way that was working was that.
I can try your approach.

return NULL;
}

$group = $runtime_context['group']->getContextData()->getValue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the context class provides you with a shorter way to do this that doesn't require traversing the data itself, by calling getContextValue directly on the context. Since that throws an exception if a required context isn't present (and group is required) we should check for hasContextValue first though.

Suggested change
$group = $runtime_context['group']->getContextData()->getValue();
if (!$runtime_context['group']->hasContextValue()) {
return NULL;
}
return $runtime_context['group']->getContextValue();

Since PHPStan likely can't entirely follow the types we can add an assert to help show the invariant.

Suggested change
$group = $runtime_context['group']->getContextData()->getValue();
if (!$runtime_context['group']->hasContextValue()) {
return NULL;
}
$group = $runtime_context['group']->getContextValue();
assert($group instanceof GroupInterface, "The group context resolver returned a context value that is not a GroupInterface instance which violates the services contract".);
return $group;

return $group;
}

if (is_int($group) === TRUE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one stylistic thing I disagree with and one functional issue in the code:

Stylistically: I'm unsure why the === TRUE is added here since that's the default for if-statements. Especially since is_int returns an exact boolean and no truthy values. if (is_int($group)) is less complex to view and equivalent so it should be preferred.

Functionally: This code will never be true. The group route service resolves the context and returns \Drupal\group\Entity\GroupInterface|null as type for the context service.

In the old code we would get the parameter from the route directly. That can be an integer in the case of views (which doesn't configure upcasting for its arguments). However, the group implementation converts that to NULL due to the following line of code in GroupRouteContextTrait::getGroupFromRoute: if (($group = $route_match->getParameter('group')) && $group instanceof GroupInterface) {.

Fixing that on our side is slightly more difficult because it means that things that are used in views (Actions/Blocks/Forms) should use the plugin context system or the form context system rather than getting the context from the route. That way they get the instance of the context that Views itself has converted into an entity. So basically anything that might need this service for this code-path, shouldn't use this service.

Comment on lines 63 to 67
$form_object = $form_state->getFormObject();

if ($form_object instanceof ContentEntityForm === FALSE) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if $form_object is not a ContentEntityForm then we have an implementation error right? So we probably want to make this an assert if we expect to catch this in testing or throw an actual exception if we think this might happen at run-time but shows an implementation error. The alternative would likely be a subtle and hard to debug bug (the post form not being altered) due to something that's wrong in an unrelated system (the post form no longer being a content entity form).

This if-statement also causes $content to be calculated but never used, so that statement should likely be after this guard clause.


if ($form_object->getEntity()->isNew()
&& $content !== NULL) {
$form['current_user_image'] = $content;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$content is only ever used here so I would suggest inlining the variable. That has the advantage that if this is a post being edited we don't need to calculate $content.

Comment on lines 84 to 88
// Default value.
$form = $this->setFormTitleAndPlaceholder($form, t('Say something to the Community'));

if ($form_state->get('currentGroup') !== NULL) {
$form = $this->setFormTitleAndPlaceholder($form, t('Say something to the group'));
}

// $user_profile = $this->routeMatch->getParameter('user');
$user_profile = $form_state->get('recipientUser');
if ($user_profile !== NULL
&& $user_profile->id() !== $this->currentUser->id()) {
$form = $this->setFormTitleAndPlaceholder($form, t('Leave a message to @name', [
'@name' => $user_profile->getDisplayName(),
]));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can re-arrange this logic to make sure we don't set the default value and then overwrite it but only set the default value if we haven't picked another value. In this case we know setFormTitleAndPlaceholder is a very light-weight function, but since it's abstracted, we really shouldn't need to know this and the function might be calculating pi or fetching expensive data from the database unnecessarily. We should not be afraid to use else if it makes sense :)

@denis-getopensocial denis-getopensocial force-pushed the feature/hux-d10.3 branch 2 times, most recently from c60df44 to cf40d65 Compare October 30, 2024 14:43
@denis-getopensocial denis-getopensocial force-pushed the feature/hux-d10.3 branch 3 times, most recently from 165685b to e62b1d6 Compare November 18, 2024 10:38
Copy link
Member

@Kingdutch Kingdutch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this Denis! Code looks good like this to me.

Once the pipelines are green this can be merged.

Copy link
Member

@bramtenhove bramtenhove left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shout out to you @denis-getopensocial for moving this forward.

Not an easy process, but I do think this is really good! Congratulations 👏

@denis-getopensocial denis-getopensocial force-pushed the feature/hux-d10.3 branch 2 times, most recently from 6da5d3b to 9ee7fbb Compare November 26, 2024 19:55
@denis-getopensocial denis-getopensocial merged commit aac7e14 into main Dec 3, 2024
195 checks passed
@denis-getopensocial denis-getopensocial deleted the feature/hux-d10.3 branch December 3, 2024 08:05
@denis-getopensocial denis-getopensocial added this to the 13.0.0-alpha19 milestone Dec 3, 2024
@@ -11,3 +11,17 @@ services:
social_post.helper:
class: Drupal\social_post\Service\SocialPostHelper
arguments: ['@entity_type.manager', '@current_user']

#hooks replacement
Copy link
Contributor Author

@denis-getopensocial denis-getopensocial Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the proper auto-wiring should looks like this (credit Alexander)

#hooks replacement
# use autowiring for arguments for `SocialPostFormHooks`
social_post.form.hooks:
  class: Drupal\social_post\Hooks\SocialPostFormHooks
  autowire: true
  tags:
  - { name: hooks }
# make `SocialPostFormHooks` injectable in another service (e.g. for decoration)
Drupal\social_post\Hooks\SocialPostFormHooks: '@social_post.form.hooks'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs work: tests This pull request is missing test coverage prio: medium status: needs review This pull request is waiting for a requested review team: guardians type: feature Adds a new feature to Open Social
Development

Successfully merging this pull request may close these issues.

4 participants